<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_Filter
 * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @version    $Id: Inflector.php 17705 2009-08-21 07:50:57Z thomas $
 */

/**
 * @see Zend_Filter
 * @see Zend_Filter_Interface
 */
require_once 'Zend/Filter.php';

/**
 * @see Zend_Loader_PluginLoader
 */
require_once 'Zend/Loader/PluginLoader.php';

/**
 * Filter chain for string inflection
 *
 * @category   Zend
 * @package    Zend_Filter
 * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
class Zend_Filter_Inflector implements Zend_Filter_Interface
{
	/**
	 * @var Zend_Loader_PluginLoader_Interface
	 */
	protected $_pluginLoader = null;

	/**
	 * @var string
	 */
	protected $_target = null;

	/**
	 * @var bool
	 */
	protected $_throwTargetExceptionsOn = true;

	/**
	 * @var string
	 */
	protected $_targetReplacementIdentifier = ':';

	/**
	 * @var array
	 */
	protected $_rules = array();

	/**
	 * Constructor
	 *
	 * @param string $target
	 * @param array $rules
	 */
	public function __construct($target = null, Array $rules = array(), $throwTargetExceptionsOn = null, $targetReplacementIdentifer = null)
	{
		if ($target instanceof Zend_Config) {
			$this->setConfig($target);
		} else {
			if ((null !== $target) && is_string($target)) {
				$this->setTarget($target);
			}

			if (null !== $rules) {
				$this->addRules($rules);
			}

			if ($throwTargetExceptionsOn !== null) {
				$this->setThrowTargetExceptionsOn($throwTargetExceptionsOn);
			}

			if ($targetReplacementIdentifer != '') {
				$this->setTargetReplacementIdentifier($targetReplacementIdentifer);
			}
		}
	}

	/**
	 * Retreive PluginLoader
	 *
	 * @return Zend_Loader_PluginLoader_Interface
	 */
	public function getPluginLoader()
	{
		if (!$this->_pluginLoader instanceof Zend_Loader_PluginLoader_Interface) {
			$this->_pluginLoader = new Zend_Loader_PluginLoader(array('Zend_Filter_' => 'Zend/Filter/'), __CLASS__);
		}

		return $this->_pluginLoader;
	}

	/**
	 * Set PluginLoader
	 *
	 * @param Zend_Loader_PluginLoader_Interface $pluginLoader
	 * @return Zend_Filter_Inflector
	 */
	public function setPluginLoader(Zend_Loader_PluginLoader_Interface $pluginLoader)
	{
		$this->_pluginLoader = $pluginLoader;
		return $this;
	}

	/**
	 * Use Zend_Config object to set object state
	 *
	 * @param  Zend_Config $config
	 * @return Zend_Filter_Inflector
	 */
	public function setConfig(Zend_Config $config)
	{
		foreach ($config as $key => $value) {
			switch ($key) {
				case 'target':
					$this->setTarget($value);
					break;
				case 'filterPrefixPath':
					if (is_scalar($value)) {
						break;
					}
					$paths = $value->toArray();
					foreach ($paths as $prefix => $path) {
						$this->addFilterPrefixPath($prefix, $path);
					}
					break;
				case 'throwTargetExceptionsOn':
					$this->setThrowTargetExceptionsOn($value);
					break;
				case 'targetReplacementIdentifier':
					$this->setTargetReplacementIdentifier($value);
					break;
				case 'rules':
					$this->addRules($value->toArray());
					break;
				default:
					break;
			}
		}
		return $this;
	}

	/**
	 * Convienence method to add prefix and path to PluginLoader
	 *
	 * @param string $prefix
	 * @param string $path
	 * @return Zend_Filter_Inflector
	 */
	public function addFilterPrefixPath($prefix, $path)
	{
		$this->getPluginLoader()->addPrefixPath($prefix, $path);
		return $this;
	}

	/**
	 * Set Whether or not the inflector should throw an exception when a replacement
	 * identifier is still found within an inflected target.
	 *
	 * @param bool $throwTargetExceptions
	 * @return Zend_Filter_Inflector
	 */
	public function setThrowTargetExceptionsOn($throwTargetExceptionsOn)
	{
		$this->_throwTargetExceptionsOn = ($throwTargetExceptionsOn == true) ? true : false;
		return $this;
	}

	/**
	 * Will exceptions be thrown?
	 *
	 * @return bool
	 */
	public function isThrowTargetExceptionsOn()
	{
		return $this->_throwTargetExceptionsOn;
	}

	/**
	 * Set the Target Replacement Identifier, by default ':'
	 *
	 * @param string $targetReplacementIdentifier
	 * @return Zend_Filter_Inflector
	 */
	public function setTargetReplacementIdentifier($targetReplacementIdentifier)
	{
		$this->_targetReplacementIdentifier = (string) $targetReplacementIdentifier;
		return $this;
	}

	/**
	 * Get Target Replacement Identifier
	 *
	 * @return string
	 */
	public function getTargetReplacementIdentifier()
	{
		return $this->_targetReplacementIdentifier;
	}

	/**
	 * Set a Target
	 * ex: 'scripts/:controller/:action.:suffix'
	 *
	 * @param string
	 * @return Zend_Filter_Inflector
	 */
	public function setTarget($target)
	{
		$this->_target = (string) $target;
		return $this;
	}

	/**
	 * Retrieve target
	 *
	 * @return string
	 */
	public function getTarget()
	{
		return $this->_target;
	}

	/**
	 * Set Target Reference
	 *
	 * @param reference $target
	 * @return Zend_Filter_Inflector
	 */
	public function setTargetReference(&$target)
	{
		$this->_target =& $target;
		return $this;
	}

	/**
	 * SetRules() is the same as calling addRules() with the exception that it
	 * clears the rules before adding them.
	 *
	 * @param array $rules
	 * @return Zend_Filter_Inflector
	 */
	public function setRules(Array $rules)
	{
		$this->clearRules();
		$this->addRules($rules);
		return $this;
	}

	/**
	 * AddRules(): multi-call to setting filter rules.
	 *
	 * If prefixed with a ":" (colon), a filter rule will be added.  If not
	 * prefixed, a static replacement will be added.
	 *
	 * ex:
	 * array(
	 *     ':controller' => array('CamelCaseToUnderscore','StringToLower'),
	 *     ':action'     => array('CamelCaseToUnderscore','StringToLower'),
	 *     'suffix'      => 'phtml'
	 *     );
	 *
	 * @param array
	 * @return Zend_Filter_Inflector
	 */
	public function addRules(Array $rules)
	{
		$keys = array_keys($rules);
		foreach ($keys as $spec) {
			if ($spec[0] == ':') {
				$this->addFilterRule($spec, $rules[$spec]);
			} else {
				$this->setStaticRule($spec, $rules[$spec]);
			}
		}

		return $this;
	}

	/**
	 * Get rules
	 *
	 * By default, returns all rules. If a $spec is provided, will return those
	 * rules if found, false otherwise.
	 *
	 * @param  string $spec
	 * @return array|false
	 */
	public function getRules($spec = null)
	{
		if (null !== $spec) {
			$spec = $this->_normalizeSpec($spec);
			if (isset($this->_rules[$spec])) {
				return $this->_rules[$spec];
			}
			return false;
		}

		return $this->_rules;
	}

	/**
	 * getRule() returns a rule set by setFilterRule(), a numeric index must be provided
	 *
	 * @param string $spec
	 * @param int $index
	 * @return Zend_Filter_Interface|false
	 */
	public function getRule($spec, $index)
	{
		$spec = $this->_normalizeSpec($spec);
		if (isset($this->_rules[$spec]) && is_array($this->_rules[$spec])) {
			if (isset($this->_rules[$spec][$index])) {
				return $this->_rules[$spec][$index];
			}
		}
		return false;
	}

	/**
	 * ClearRules() clears the rules currently in the inflector
	 *
	 * @return Zend_Filter_Inflector
	 */
	public function clearRules()
	{
		$this->_rules = array();
		return $this;
	}

	/**
	 * Set a filtering rule for a spec.  $ruleSet can be a string, Filter object
	 * or an array of strings or filter objects.
	 *
	 * @param string $spec
	 * @param array|string|Zend_Filter_Interface $ruleSet
	 * @return Zend_Filter_Inflector
	 */
	public function setFilterRule($spec, $ruleSet)
	{
		$spec = $this->_normalizeSpec($spec);
		$this->_rules[$spec] = array();
		return $this->addFilterRule($spec, $ruleSet);
	}

	/**
	 * Add a filter rule for a spec
	 *
	 * @param mixed $spec
	 * @param mixed $ruleSet
	 * @return void
	 */
	public function addFilterRule($spec, $ruleSet)
	{
		$spec = $this->_normalizeSpec($spec);
		if (!isset($this->_rules[$spec])) {
			$this->_rules[$spec] = array();
		}

		if (!is_array($ruleSet)) {
			$ruleSet = array($ruleSet);
		}

		if (is_string($this->_rules[$spec])) {
			$temp = $this->_rules[$spec];
			$this->_rules[$spec] = array();
			$this->_rules[$spec][] = $temp;
		}

		foreach ($ruleSet as $rule) {
			$this->_rules[$spec][] = $this->_getRule($rule);
		}

		return $this;
	}

	/**
	 * Set a static rule for a spec.  This is a single string value
	 *
	 * @param string $name
	 * @param string $value
	 * @return Zend_Filter_Inflector
	 */
	public function setStaticRule($name, $value)
	{
		$name = $this->_normalizeSpec($name);
		$this->_rules[$name] = (string) $value;
		return $this;
	}

	/**
	 * Set Static Rule Reference.
	 *
	 * This allows a consuming class to pass a property or variable
	 * in to be referenced when its time to build the output string from the
	 * target.
	 *
	 * @param string $name
	 * @param mixed $reference
	 * @return Zend_Filter_Inflector
	 */
	public function setStaticRuleReference($name, &$reference)
	{
		$name = $this->_normalizeSpec($name);
		$this->_rules[$name] =& $reference;
		return $this;
	}

	/**
	 * Inflect
	 *
	 * @param  string|array $source
	 * @return string
	 */
	public function filter($source)
	{
		// clean source
		foreach ( (array) $source as $sourceName => $sourceValue) {
			$source[ltrim($sourceName, ':')] = $sourceValue;
		}

		$pregQuotedTargetReplacementIdentifier = preg_quote($this->_targetReplacementIdentifier, '#');
		$processedParts = array();

		foreach ($this->_rules as $ruleName => $ruleValue) {
			if (isset($source[$ruleName])) {
				if (is_string($ruleValue)) {
					// overriding the set rule
					$processedParts['#'.$pregQuotedTargetReplacementIdentifier.$ruleName.'#'] = str_replace('\\', '\\\\', $source[$ruleName]);
				} elseif (is_array($ruleValue)) {
					$processedPart = $source[$ruleName];
					foreach ($ruleValue as $ruleFilter) {
						$processedPart = $ruleFilter->filter($processedPart);
					}
					$processedParts['#'.$pregQuotedTargetReplacementIdentifier.$ruleName.'#'] = str_replace('\\', '\\\\', $processedPart);
				}
			} elseif (is_string($ruleValue)) {
				$processedParts['#'.$pregQuotedTargetReplacementIdentifier.$ruleName.'#'] = str_replace('\\', '\\\\', $ruleValue);
			}
		}

		// all of the values of processedParts would have been str_replace('\\', '\\\\', ..)'d to disable preg_replace backreferences
		$inflectedTarget = preg_replace(array_keys($processedParts), array_values($processedParts), $this->_target);

		if ($this->_throwTargetExceptionsOn && (preg_match('#(?='.$pregQuotedTargetReplacementIdentifier.'[A-Za-z]{1})#', $inflectedTarget) == true)) {
			require_once 'Zend/Filter/Exception.php';
			throw new Zend_Filter_Exception('A replacement identifier ' . $this->_targetReplacementIdentifier . ' was found inside the inflected target, perhaps a rule was not satisfied with a target source?  Unsatisfied inflected target: ' . $inflectedTarget);
		}

		return $inflectedTarget;
	}

	/**
	 * Normalize spec string
	 *
	 * @param  string $spec
	 * @return string
	 */
	protected function _normalizeSpec($spec)
	{
		return ltrim((string) $spec, ':&');
	}

	/**
	 * Resolve named filters and convert them to filter objects.
	 *
	 * @param  string $rule
	 * @return Zend_Filter_Interface
	 */
	protected function _getRule($rule)
	{
		if ($rule instanceof Zend_Filter_Interface) {
			return $rule;
		}

		$rule = (string) $rule;

		$className  = $this->getPluginLoader()->load($rule);
		$ruleObject = new $className();
		if (!$ruleObject instanceof Zend_Filter_Interface) {
			require_once 'Zend/Filter/Exception.php';
			throw new Zend_Filter_Exception('No class named ' . $rule . ' implementing Zend_Filter_Interface could be found');
		}

		return $ruleObject;
	}

}
